/* vim:ft=objc:
 * $Id: IMAPServer.m 154 2006-08-20 18:51:20Z brandenm $
 *
 * Copyright (c) 2004, 2005  Branden J. Moore.
 *
 * This file is part of MacBiff, and is free software; you can redistribute
 * it and/or modify it under the terms of the GNU General Public License
 * version 2 as published by the Free Software Foundation.
 *
 * MacBiff is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with MacBiff; if not, write to the Free Software Foundation, Inc., 59
 * Temple Place, Suite 330, Boston, MA  02111-1307 USA.
 *
 */

#import <AppKit/NSNibLoading.h>
#import "IMAPServer.h"
#import "IMAPResponse.h"
#import "IMAPException.h"

#if 0
#  define EBUG 1
#endif
#import <MacBiffPlugIn/debug.h>
#import <MacBiffPlugIn/xmalloc.h>
#import <MacBiffPlugIn/PreferencesController.h>
#import <MacBiffPlugIn/Keychain.h>
#import <MacBiffPlugIn/Folder.h>
#import <MacBiffPlugIn/Message.h>

#include <errno.h>
#include <stdlib.h>
#include <ctype.h>

#define dwrite(a,b) \
	if ( (EBUG) || print_debug & NETWORK_DEBUG ) do { \
		fprintf(_dbgfp, "%s%s", (a), (b)); \
	} while(0)

@interface NSArray (StringFind)
- (BOOL) containsString: (NSString*) str options: (unsigned) mask;
@end

@implementation NSArray (StringFind)
- (BOOL) containsString: (NSString*) str options: (unsigned) mask
{
	BOOL found = NO;
	NSString *thisString = Nil;
	NSEnumerator *arrEnum = [self objectEnumerator];

	while ( !found && (thisString = [arrEnum nextObject]) ) {
		NSComparisonResult res;
		res = [thisString compare: str options: mask];

		found = (res == NSOrderedSame);
	}

	return found;
}
@end

char* eatQuotedString(char** ptr)
{
	char *p = *ptr;
	char *start = *ptr;

	dprintf("%s Eating string: '%s'\n", __func__, p);
	if ( *p != '\"' ) {
		if ( *p == 'N' && *(p+1) == 'I' && *(p+2) == 'L' ) {
			p++; /* N */
			p++; /* I */
			p++; /* L */
			*ptr = p;
		}
		return "";
	}

	p++;
	start++;

	while ( *p && (*p != '\"' || (*(p-1) == '\\' && *(p-2) != '\\')) ) p++;

	*p = '\0';
	*ptr = ++p;
	dprintf("returning string: '%s'\n", start);
	return start;
}

/* Eat an entire parenthized email array, return the first address*/
char* eatParenthizedEmail(char **ptr)
{
	char *start = *ptr;
	char *end = *ptr;
	char *p = *ptr;
	int pcount = 0;

	dprintf("%s Eating string: '%s'\n", __func__, p);

	if ( *p == 'N' && *(p+1) == 'I' && *(p+2) == 'L' ) {
		p++; /* N */
		p++; /* I */
		p++; /* L */
		*ptr = p;
		return "";
	}
	if ( *p != '(' ) return "";
	pcount++;
	p++;

	if ( *p == '(' ) { /* This *should* always be */
		/* Start processing the email */
		pcount++;
		p++;

		/* Name */
		if ( *p == '\"' ) {
			start = eatQuotedString(&p);
			start--; /* Back up and add a quote */
			*start = '\"';
				/* Add a quote on to the end */
			end = ++p;
			*(end-2) = '\"';
			*(end-1) = ' ';
		} else {
			p++; /* N */
			p++; /* I */
			p++; /* L */
			p++; /*   */
		}

		/* Source Route (?) */
		if ( *p == '\"' ) {
			eatQuotedString(&p);
			p++;
		} else {
			p++; /* N */
			p++; /* I */
			p++; /* L */
			p++; /*   */
		}

		/* MailBox (who part of from */
		if ( *p == '\"' ) {
			char *addr = eatQuotedString(&p);
			*end = '<'; end++;
			while ( *addr ) {
				*end = *addr;
				end++;
				addr++;
			}
			p++; /* Eat the space */
		} else {
			p++; /* N */
			p++; /* I */
			p++; /* L */
			p++; /*   */
		}

		/* domain (where part of from */
		if ( *p == '\"' ) {
			char *addr = eatQuotedString(&p);
			*end = '@'; end++;
			while ( *addr ) {
				*end = *addr;
				end++;
				addr++;
			}
			*end = '>'; end++;
			*end = '\0';
		} else {
			p++; /* N */
			p++; /* I */
			p++; /* L */
		}
		if ( *p != ')' ) {
			dprintf("Processing went bad. returning '' string.\n");
			start = "";
		} else {
			pcount--;
		}
	}
	while ( (pcount != 0) && *p ) {
		p++;
		if ( *p == '(' ) pcount++;
		if ( *p == ')' ) pcount--;
	}

	*ptr = ++p;
	dprintf("%s Returning: '%s'\n", __func__, start);
	return start;
}


/* From RFC 3501:
 *
 *  (DATE SUBJECT FROM SENDER REPLY-TO TO CC BCC IN-REPLY-TO MESSAGE-ID)
 *  ((name NIL user domain)(name NIL user domain))
 */
NSDictionary* parseIMAPEnvelope(NSString *env)
{
	NSMutableDictionary *dict;
	dict = [[NSMutableDictionary alloc] initWithCapacity: 5];
	char *buf = Nil;
	char *ptr = Nil;
	char *str = Nil;
	int len = [env cStringLength] + 1;

	xmalloc(buf, char, len);
	[env getCString: buf maxLength: len-1];

	ptr = buf;
	ptr++; /* Advance off the inital '(' */
	/* --DATE-- */
	str = eatQuotedString(&ptr);
	if ( !str || *str == '\0' ) { /* Invalid message.  Can't deal with it */
		return NULL;
	}

	NSMutableString *dstr = [[[NSMutableString alloc] init] autorelease];
	[dstr setString: [NSString stringWithCString: str]];
	NSCalendarDate *cd = [NSCalendarDate dateWithNaturalLanguageString: dstr];
	if ( cd == Nil ) {
		/* Attempt to take care of a funky date format for NSDate */
		[dstr replaceOccurrencesOfString: @"." withString: @"/"
				options: NSLiteralSearch
				range: NSMakeRange(0, [dstr length])];
		cd = [NSCalendarDate dateWithNaturalLanguageString: dstr];
		if ( cd == Nil ) {
			cd = [NSDate date]; /* Empty */
		}
	}
	[dict setObject: cd forKey: @"Date"];

	/* --SUBJECT-- */
	ptr++; /* Advance off the space */
	str = eatQuotedString(&ptr);
	[dict setObject: [NSString stringWithCString: str]
		 forKey: @"Subject"];

	/* --FROM-- */
	ptr++; /* Advance off the space */
	str = eatParenthizedEmail(&ptr);
	if ( *str == '\"' ) {
		/* Has short name */
		char *p = str;
		[dict setObject: [NSString stringWithCString:
			eatQuotedString(&p)]
			forKey: @"FromName"];
		p++; /* Eat off space */
		NSString *em = [NSString stringWithCString: p];
		em = [em substringWithRange: NSMakeRange(1, [em length]-2)];
		[dict setObject: em forKey: @"FromEmail"];
	} else {
		NSString *em = [NSString stringWithCString: str];
		em = [em substringWithRange: NSMakeRange(1, [em length]-2)];
		/* Does not have short name*/
		[dict setObject: em forKey: @"FromName"];
		[dict setObject: em forKey: @"FromEmail"];
	}
	dprintf("short name: '%s' email: '%s'\n",
		[[dict objectForKey: @"FromName"] cString],
		[[dict objectForKey: @"FromEmail"] cString]);

	[dict setObject: [NSString stringWithCString: str]
		 forKey: @"From"];

	/* --SENDER-- */
	ptr++; /* Advance off the space */
	str = eatParenthizedEmail(&ptr);
	[dict setObject: [NSString stringWithCString: str]
		 forKey: @"Sender"];

	/* --REPLY-TO-- */
	ptr++; /* Advance off the space */
	str = eatParenthizedEmail(&ptr);
	[dict setObject: [NSString stringWithCString: str]
		 forKey: @"Reply-To"];

	/* --TO-- */
	ptr++; /* Advance off the space */
	str = eatParenthizedEmail(&ptr);
	[dict setObject: [NSString stringWithCString: str]
		 forKey: @"To"];

	/* --CC-- */
	ptr++; /* Advance off the space */
	str = eatParenthizedEmail(&ptr);
	[dict setObject: [NSString stringWithCString: str]
		 forKey: @"CC"];

	/* --BCC-- */
	ptr++; /* Advance off the space */
	str = eatParenthizedEmail(&ptr);
	[dict setObject: [NSString stringWithCString: str]
		 forKey: @"BCC"];

	/* --IN-REPLY-TO-- */
	ptr++; /* Advance off the space */
	str = eatQuotedString(&ptr);
	[dict setObject: [NSString stringWithCString: str]
		 forKey: @"In-Reply-To"];

	/* --MESSAGE-ID-- */
	ptr++; /* Advance off the space */
	str = eatQuotedString(&ptr);
	[dict setObject: [NSString stringWithCString: str]
		 forKey: @"Message-ID"];

	xfree(buf);
	return [dict autorelease];
}



@implementation IMAPServer

+ (NSString *) plugInName
{
	return @"IMAP";
}


+ (NSString *) plugInVersion
{
	return @"1.0.0";
}


+ (NSString *) plugInAuthor
{
	return @"Branden J. Moore <bmoore@forkit.org>";
}


+ (NSString *) plugInUrl
{
	return @"http://www.forkit.org/macbiff/";
}


- (NSString*) escapequotes: (NSString*) s
{
	NSMutableString *ms = [[NSMutableString alloc] initWithString: s];

	/* un-escape already escaped quotes */
	[ms replaceOccurrencesOfString: @"\\\"" withString: @"\""
		options: NSLiteralSearch|NSBackwardsSearch
		range: NSMakeRange(0, [ms length])];
	[ms replaceOccurrencesOfString: @"\"" withString: @"\\\""
		options: NSLiteralSearch|NSBackwardsSearch
		range: NSMakeRange(0, [ms length])];
	return [ms autorelease];
}


- (void) statusAt: (double) pos withText: (NSString*) txt
{
	if ( actControl ) {
		[actControl updateStatus: pos withText: txt];
	}
}


- (void) statusBy: (double) pos withText: (NSString*) txt
{
	if ( actControl ) {
		[actControl incrementStatus: pos withText: txt];
	}
}


/* IMAP login & query-response procedures */
- (IMAPResponse*) getResponseThroughUID: (unsigned int) uid
{
	IMAPResponse *resp = [[IMAPResponse alloc] init];
	NSString *targetToken = [NSString stringWithFormat: @"%05u ", uid];
	BOOL serverSaidBYE = NO;
	NSString *str = Nil;
	errno = 0;

	do {
		str = [io readLine];
		if ( !str ) break;
		dwrite("< ", [str cString]);

		/* Check for a literal string. */
		while ( [str hasSuffix: @"}\r\n"] ) {
			dprintf("Parsing a Literal String.\n");
			NSString *litstr = Nil;
			int literalcount = 0;
			NSRange literalRange = [str rangeOfString: @"{"
					options: NSBackwardsSearch];
			if ( literalRange.location != NSNotFound ) {
				int startOfSuffix = literalRange.location;
				literalRange.location++;
				literalRange.length = [str length] - 3 -
					literalRange.location;
				literalcount = [[str substringWithRange:
					literalRange] intValue];
				/* Strip the literal suffix */
				str = [str substringWithRange:
					NSMakeRange(0, startOfSuffix)];

				dprintf("Length of literal: %d\n",literalcount);

				/* Now, the next literalcount bytes are the
				 * literal string.  Need to concatentate them
				 * onto this string. */
				str = [str stringByAppendingString: @"\""];
				do {
					litstr = [io readLine];
					if ( !litstr ) break;
					dwrite("< ", [litstr cString]);
					dprintf("read %d characters.\n",
							[litstr length]);

					if ( literalcount >= (int)[litstr length] ) {
						str = [str stringByAppendingString: [self escapequotes: litstr]];
						literalcount -= [litstr length];
						if ( !literalcount )
							str = [str stringByAppendingString: @"\""];
					} else {
						/* Only part of this line is "literal" */
						literalRange = NSMakeRange(0, literalcount);
						str = [str stringByAppendingString: [self escapequotes: [litstr substringWithRange: literalRange]]];
						literalRange.location = literalRange.length;
						literalRange.length = [litstr length] - literalRange.location;
						str = [str stringByAppendingString: @"\""];
						str = [str stringByAppendingString: [litstr substringWithRange: literalRange]];
						literalcount = 0;
					}
				} while ( literalcount > 0 );
				dprintf("Literal ended as: %s", [str cString]);
			} else {
				dprintf("False Alarm, I guess?\n");
			}
		}

		if ( [str hasPrefix: @"* BYE"] ) {
			dprintf("Found a BYE...\n");
			serverSaidBYE = YES;
		}

		[resp addLine: str];
		/*dprintf("Added Line... Looping if doesn't start with: '%s'\n",
		 *		[targetToken cString]);
		 */
	} while ( ![str hasPrefix: targetToken] );

	if ( serverSaidBYE ) {
		dwrite("", "Server closed the connection.\n");
		[io disconnect];
	}

	if ( errno == EINTR ) {
		[resp release];
		[IMAPException raiseStop];
	} else if ( errno == ECONNRESET ) {
		[resp release];
		[IMAPException raiseReset];
	}

	return [resp autorelease];
}


- (IMAPResponse*) query: (NSString*) s
{
	IMAPResponse *resp = Nil;
	errno = 0;

	NSString *str = [NSString stringWithFormat: @"%05u %@\r\n",
		 cmduid++, s];
	dwrite( "> ", [str cString] );
	[io writeLine: str];
	if ( stop_now ) {
		[IMAPException raiseStop];
	} else if ( errno == ECONNRESET ) {
		[IMAPException raiseReset];
	}

	resp = [self getResponseThroughUID: cmduid-1];

	return resp;
}


/* login:  Returns 0 if successfully logged in.  -1 otherwise, & sets errno */

- (int) connect
{
	int res;
	IMAPResponse *resp;
	BOOL isPreAuth = NO;
	NSRange range;

	dprintf("Connecting with SSL: %s\n", [[mPrefs objectForKey:@"Use SSL"] boolValue] ? "YES" : "NO");
	res = [io connectWithSSL: [[mPrefs objectForKey:@"Use SSL"] boolValue]];
	if ( res != 0 ) {
		alert("Failed to Connect\n");
		return res;
	}

	cmduid = 0;

	/* Verify Appropriate Server and pre-authenticated state */
	NSString *str = [io readLine];
	range = [str rangeOfString: @"Pre-authenticated user"
		options: NSCaseInsensitiveSearch];
	if ( range.location != NSNotFound ) {
		isPreAuth = YES;
	}

	range = [str rangeOfString: @"IMAP4REV1"
			options: NSCaseInsensitiveSearch];
	if ( range.location == NSNotFound ) {
		BOOL isImap4 = NO;
		/* Doesn't advertise IMAP 4rev1, query for it */
		resp = [self query: @"CAPABILITY"];
		if ( !resp || ![resp isOK] ) {
			[io disconnect];
			return -1;
		}
		while ( (str = [resp nextLine]) ) {
			range = [str rangeOfString: @"IMAP4REV1"
				options: NSCaseInsensitiveSearch];
			if ( range.location != NSNotFound ) {
				isImap4 = YES;
			}
			range = [str rangeOfString: @"Pre-authenticated user"
				options: NSCaseInsensitiveSearch];
			if ( range.location != NSNotFound ) {
				isPreAuth = YES;
			}
		}
		if ( !isImap4 ) {
			[io disconnect];
			errno = EPFNOSUPPORT;
			return -1;
		}
	}

	/* Log in if needed */
	if ( !isPreAuth ) {
		dwrite("> ", "XXXXX LOGIN Username \"XXXXXXXX\"\r\n");
		str = [NSString stringWithFormat: @"%05u LOGIN %@ \"%@\"\r\n",
			cmduid++, [mPrefs objectForKey: @"Username"],
			passwd];
		[io writeLine: str];
		resp = [self getResponseThroughUID: cmduid-1];
		if ( ![resp isOK] ) {
			NSAlert *alert = [NSAlert alertWithMessageText:
					@"Login Failed!"
				defaultButton: Nil
				alternateButton: Nil
				otherButton: Nil
				informativeTextWithFormat:
				@"Login to %@ Failed!\n%@",
				acctName,
				[resp nextLine]];
			[alert runModal];
			errno = EAUTH;
			[io disconnect];
			return -1;
		}
	}

	return 0;
}



- (Folder*) insertFolder: (NSString*) fname withFlags: (NSArray*) flags
{
	NSMutableArray *follist = folders;
	NSArray *components = [fname componentsSeparatedByString: sep];
	NSEnumerator *compEnum = [components objectEnumerator];
	NSString *component = Nil;
	Folder *folder = Nil;
	int compCount = 0;

	dprintf("Inserting folder '%s'\n", [fname cString]);
	while ( (component = [compEnum nextObject]) ) {
		compCount++;
		if ( [component isEqual: @""] ) continue;

		int idx = [follist indexOfObject: component];
		if ( idx == NSNotFound ) {
			/* Must add this level */
			NSString *fullpath = [[components subarrayWithRange:
				       		NSMakeRange(0, compCount)]
				       componentsJoinedByString: sep];
			folder = [[Folder alloc] initWithName: component
				andFullPath: fullpath];
			[folder setCanContainMail: NO];

			dprintf("Adding component '%s' of '%s'\n",
					[component cString], [fname cString]);
			[follist addObject: folder];
			[follist sortUsingSelector: @selector(compare:)];
			follist = [folder subFolders];
		} else {
			folder = [follist objectAtIndex: idx];
			follist = [folder subFolders];
		}
	}

	if ( !folder ) {
		alert("Never found or inserted '%s' odd...\n", [fname cString]);
		return Nil;
	}

	/* Last component, this is us */
	if ( !flags || ![flags containsString: @"\\NoSelect"
			options: NSCaseInsensitiveSearch] ) {
		[folder setCanContainMail: YES];
	}

	return folder;
}


- (int) readFolders: (IMAPResponse*) resp
{
	NSString *line = Nil;
	BOOL setDelimiter = NO;
	int count = 0;

	[resp resetEnumerator];

	while ( (line = [resp nextLine]) && [line hasPrefix: @"* "] ) {
		NSArray *flags = Nil;
		NSString *delim = Nil;
		NSString *fname = Nil;
		NSRange range;
		int pos;
		line = [line stringByTrimmingCharactersInSet:
			[NSCharacterSet whitespaceAndNewlineCharacterSet]];

		dprintf("Working on line: '%s'\n", [line cString]);

		/* Find flags */
		range = [line rangeOfString: @"("];
		pos = range.location + 1;
		range = [line rangeOfString: @")"];
		range.length = range.location - pos;
		range.location = pos;
		flags = [[line substringWithRange: range]
				componentsSeparatedByString: @" "];

		/* Strip everything that preceeds what we want to find */
		range.location = range.location + range.length + 2;
		range.length = [line length] - range.location;
		line = [line substringWithRange: range];

		/* Find Delimiter */
		if ( !setDelimiter ) {
			range = NSMakeRange(0, 3);

			delim = [line substringWithRange: range];
			if ( [delim caseInsensitiveCompare: @"NIL"] == NSOrderedSame ) {
				delim = [delim uppercaseString];
			} else {
				range = NSMakeRange(1, 1);
				delim = [delim substringWithRange: range];
			}
			if ( sep ) [sep release];
			sep = [delim copy];
			[mPrefs setObject: sep forKey: @"Folder Separator"];
			setDelimiter = YES;
		}
		/* Strip off delimiter stuff */
		range = NSMakeRange(4, [line length] - 4);
		line = [line substringWithRange: range];

		/* Find name */
		fname = [line stringByTrimmingCharactersInSet:
			[NSCharacterSet characterSetWithCharactersInString: @"\""]];

		dprintf("Found %d flags, delimeter of: '%s' and name: '%s'\n",
			[flags count], [sep cString], [fname cString]);

		[self insertFolder: fname withFlags: flags];
		count++;
	}
	return count;
}


/* Read in the preferences, or set up the defaults */
- (void) initializePrefs
{
	maskedFolders = [[NSMutableSet alloc] init];
	disabledFolders = [[NSMutableSet alloc] init];
	if ( ![mPrefs objectForKey: @"Hostname"] ) {
		/* No preferences set.  Set defaults */
		[mPrefs setObject: [maskedFolders allObjects] forKey: @"Masked Folders"];
		[mPrefs setObject: [disabledFolders allObjects] forKey: @"Disabled Folders"];
		[mPrefs setObject: [NSNumber numberWithBool: YES] forKey: @"Fetch Headers"];
		[mPrefs setObject: [NSNumber numberWithBool: YES] forKey: @"Fetch Masked Headers"];
		[mPrefs setObject: @"/" forKey: @"Folder Separator"];
		[mPrefs setObject: @"" forKey: @"Hostname"];
		[mPrefs setObject: @"" forKey: @"Username"];
		[mPrefs setObject: @"" forKey: @"Path Prefix"];
		[mPrefs setObject: [NSNumber numberWithInt: 993] forKey: @"Port"];
		[mPrefs setObject: [NSNumber numberWithBool: YES] forKey: @"Use Default Actions"];
		[mPrefs setObject: [NSNumber numberWithBool: YES] forKey: @"Use SSL"];
	} else {
		[maskedFolders addObjectsFromArray:
			[mPrefs objectForKey: @"Masked Folders"]];
		[disabledFolders addObjectsFromArray:
			[mPrefs objectForKey: @"Disabled Folders"]];
	}

	sep = [[mPrefs objectForKey: @"Folder Separator"] copy];
	NSString *tpw = [keychain getPasswordForUser: [mPrefs objectForKey: @"Username"]
		onHost: [mPrefs objectForKey: @"Hostname"]
		atPort: [[mPrefs objectForKey: @"Port"] intValue]
		withProto: ([[mPrefs objectForKey: @"Use SSL"] boolValue]) ?
			kSecProtocolTypeIMAPS : kSecProtocolTypeIMAP
		byAsking: NO];

	if ( tpw ) {
		passwd = [[NSString alloc] initWithString: tpw];
	} /* Else, ask on first check */

	NSEnumerator *listEnum = [maskedFolders objectEnumerator];
	NSString *folName;
	Folder *folder;

	[self insertFolder: [NSString stringWithString: @"INBOX"] withFlags: Nil];
	while ( (folName = [listEnum nextObject]) ) {
		folder = [self insertFolder: folName withFlags: Nil];
		[folder setMasked: YES recursively: NO];
	}

	listEnum = [disabledFolders objectEnumerator];
	while ( (folName = [listEnum nextObject]) ) {
		folder = [self insertFolder: folName withFlags: Nil];
		[folder setDisabled: YES recursively: NO];
	}

}


- (id) initWithName: (NSString*) n
{
	if ( ( self = [super initWithName: n] ) != Nil ) {
		dprintf("Initializing IMAP...\n");
		if ( ![NSBundle loadNibNamed: @"imap" owner: self] ) {
			alert("Unable to Load IMAP Nib!\n");
		}
		mPrefs = [prefs account: n];
		folders = [[NSMutableArray alloc] init];
		[self initializePrefs];
	}
	return self;
}


- (void) setupView
{
	[checkSubscribedButton setState:
		[[mPrefs objectForKey: @"Subscribed Folders Only"] boolValue] ?
			NSOnState : NSOffState];
	[fetchHeadersButton setState:
		[[mPrefs objectForKey: @"Fetch Headers"] boolValue] ?
			NSOnState : NSOffState];
	[fetchMaskedHeadersButton setState:
		[[mPrefs objectForKey: @"Fetch Masked Headers"] boolValue] ?
			NSOnState : NSOffState];
	[loginNameTextField setStringValue:
		[mPrefs objectForKey: @"Username"]];
	[passwordTextField setStringValue: passwd ? passwd : @""];
	[portTextField setIntValue:
		[[mPrefs objectForKey: @"Port"] intValue]];
	[prefixTextField setStringValue:
		[mPrefs objectForKey: @"Path Prefix"]];
	[serverNameTextField setStringValue:
		[mPrefs objectForKey: @"Hostname"]];
	[useSSLButton setState:
		[[mPrefs objectForKey: @"Use SSL"] boolValue] ?
			NSOnState : NSOffState];
}


- (Message*) fetchMessageWithUID: (unsigned int) uid
{
	IMAPResponse *resp = Nil;
	NSString *fetchline = Nil;
	NSString *envString = Nil;
	NSRange range;
	NSDictionary *rfc822header = Nil;
	Message *msg = Nil;

	dprintf("Fetching header for UID: %u\n", uid);

	resp = [self query: [NSString stringWithFormat:
		@"UID FETCH %u ENVELOPE", uid]];
	if ( !resp || ![resp isOK] ) {
		alert("Unable to fetch message UID: %u\n", uid);
		return Nil;
	}

	fetchline = [resp lineContaining: @"FETCH ("];
	if ( !fetchline ) {
		alert("Fetch did not return FETCH line.  BAD\n");
		return Nil;
	}

	range = [fetchline rangeOfString: @"ENVELOPE ("];
	range.location += (range.length -1);
	range.length = [fetchline length] - range.location;
	envString = [fetchline substringWithRange: range];

	/*BJM:  Parse the response to get the rfc822 headers */
	rfc822header = parseIMAPEnvelope(envString);
	if ( !rfc822header ) {
		return Nil;
	}

	msg = [Message newMessageFrom: [rfc822header objectForKey: @"FromName"]
				email: [rfc822header objectForKey: @"FromEmail"]
			  withSubject: [rfc822header objectForKey: @"Subject"]
			       atDate: [rfc822header objectForKey: @"Date"]
			      withUid: uid];

	return msg;
}


- (void) fetchMessages: (NSArray*) uidArr
      andPlaceInFolder: (Folder*) folder
{
	unsigned int fol_total = 0, fol_unread = 0, fol_recent = 0;
	Message *uidMsg = Nil;
	Message *msg = Nil;
	NSString *uidString;
	NSArray *oldMsgList = [[folder messageList] copy];
	NSEnumerator *uidEnum = [uidArr objectEnumerator];
	unsigned int uid = 0;
	int idx;

	dprintf("Fetching headers in: %s\n", [[folder fullPath] cString]);

	/* This resets the message counts.  Save 'em first */
	fol_total = [folder totalMail];
	fol_unread = [folder unreadMail];
	fol_recent = [folder newMail];
	[folder resetMessageList];

	/* Copy over any still unread messages, fetch those that are new */
	uidMsg = [[Message alloc] init];
	while ( (uidString = [uidEnum nextObject]) ) {
		uid = (unsigned int)[uidString intValue];
		[uidMsg setUID: uid];
		if ( (idx = [oldMsgList indexOfObject: uidMsg]) != NSNotFound ) {
			msg = [oldMsgList objectAtIndex: idx];
			/* It's no longer *NEW*, we've seen it before */
			[msg setNew: NO];
			[folder addMessage: msg];
		} else {
			msg = [self fetchMessageWithUID: uid];
			if ( msg ) {
				[folder addMessage: msg];
			}
		}
	}

	if ( ([folder unreadMail] != fol_unread) ||
			([folder newMail] != fol_recent) ) {
		dprintf("Huh... Thought we had %u unread.  Now: %u.  "
				   "Thought we had %u new.  Now: %u\n",
				   fol_unread, [folder unreadMail],
				   fol_recent, [folder newMail]);
	}

	[folder setTotal: fol_total];
	[oldMsgList release];
}


- (void) checkFolder: (Folder*) folder
{
	IMAPResponse *resp = Nil;
	NSArray *tArray = Nil;
	NSString *tString = Nil;

	dprintf("Checking Folder: '%s' (%s)\n",
			[[folder name] cString], [[folder fullPath] cString]);

	resp = [self query: [NSString stringWithFormat: @"EXAMINE \"%@\"",
					[folder fullPath]]];
	if ( ![resp isOK] ) {
		alert("Unable to examine '%s'\n", [[folder fullPath] cString]);
		[folder setCanContainMail: NO];
		return;
	}

	/* Get Total count for folder */
	tString = [resp lineContaining: @"EXISTS"];
	if ( !tString ) {
		alert("Improper IMAP Response to EXAMINE.\n");
		[folder setCanContainMail: NO];
		return;
	}
	tArray = [tString componentsSeparatedByString: @" "];
	[folder setTotal: [[tArray objectAtIndex: 1] intValue]];

	/* Get Unseen count for Folder */
	tString = [resp lineContaining: @"RECENT"];
	if ( !tString ) {
		alert("Improper IMAP Response to EXAMINE.\n");
		[folder setCanContainMail: NO];
		return;
	}
	tArray = [tString componentsSeparatedByString: @" "];
	[folder setNew: [[tArray objectAtIndex: 1] intValue]];


	/* Get Unseen count for Folder */
	resp = [self query: @"UID SEARCH UNSEEN"];
	if ( ![resp isOK] ) {
		alert("IMAP Server is Broken.  No UID SEARCH UNSEEN.\n");
		return;
	}
	tString = [resp lineContaining: @"SEARCH"];
	tArray = [tString componentsSeparatedByString: @" "];
	tArray = [tArray subarrayWithRange: NSMakeRange(2, [tArray count]-2)];
	[folder setUnread: [tArray count]];

	if ( [[mPrefs objectForKey: @"Fetch Headers"] boolValue] &&
		( ![folder masked] ||
		  [[mPrefs objectForKey: @"Fetch Masked Headers"] boolValue])){

		[self fetchMessages: tArray andPlaceInFolder: folder];

	}

	newTotal += [folder totalMail];
	if ( ![folder masked] ) {
		newNew += [folder newMail];
		newUnread += [folder unreadMail];
	}
}


- (void) checkFolders: (NSArray*) list inc: (double) incr
{
	Folder *folder = Nil;
	NSEnumerator *folEnum = [list objectEnumerator];

	while ( (folder = [folEnum nextObject]) ) {
		if ( [folder canContainMail] && ![folder disabled] ) {
			[self statusBy: incr withText: [folder fullPath]];
			[self checkFolder: folder];
		} else {
			dprintf("Skipping folder '%s'\n",
				[[folder fullPath] cString]);
			[self statusBy: incr withText: [folder fullPath]];
		}
		[self checkFolders: [folder subFolders] inc: incr];

		if ( [folder masked] ) {
			[maskedFolders addObject: [folder fullPath]];
		} else {
			[maskedFolders removeObject: [folder fullPath]];
		}
		if ( [folder disabled] ) {
			[disabledFolders addObject: [folder fullPath]];
		} else {
			[disabledFolders removeObject: [folder fullPath]];
		}
	}

}


- (int) checkMail
{
	int retval = 0;
	int res;
	int fc = 1;
	NSAutoreleasePool *mPool = [[NSAutoreleasePool alloc] init];
	IMAPResponse *resp = Nil;
	BOOL normal_end = YES;

	stop_now = 0;
	newNew = newTotal = newUnread = 0;

	dprintf("Account '%s' to check mail.\n", [acctName cString]);
	/* Verify if we have a password */
	if ( !passwd ) {
		NSString *tpw = [keychain getPasswordForUser:
				[mPrefs objectForKey: @"Username"]
			onHost: [mPrefs objectForKey: @"Hostname"]
			atPort: [[mPrefs objectForKey: @"Port"] intValue]
			withProto: ([[mPrefs objectForKey: @"Use SSL"] boolValue]) ?
				kSecProtocolTypeIMAPS : kSecProtocolTypeIMAP
			byAsking: YES];
		passwd = [tpw retain];
	}

	if ( !io ) {
		io = [[LineIO alloc] initWithHost:
				[mPrefs objectForKey: @"Hostname"]
			andPort: [[mPrefs objectForKey: @"Port"] intValue]];
	}
	@try {
	/* Connect */
		[self statusAt: 0.0 withText: @"Connecting..."];
		res = [self connect];

	/* List Folders */
		[self statusAt: 5.0 withText: @"Getting Folder List..."];
		NSString *str = [NSString stringWithFormat: @"%@ \"%@\" \"*\"",
				[[mPrefs objectForKey:
					@"Subscribed Folders Only"] boolValue] ?
					@"LSUB" : @"LIST",
				[mPrefs objectForKey: @"Path Prefix"]];
		resp = [self query: str];
		if ( [resp isOK] ) {
			fc = [self readFolders: resp];
	/* Check Mail */
			if ( fc ) {
				[self checkFolders: folders
					       inc: (90.0/(double)fc)];
			}

		}

	/* Log Out */
		[self statusAt: 95.0 withText: @"Logging Out."];
		dprintf("Logging out...\n");
		resp = [self query: @"LOGOUT"];
	}
	@catch ( IMAPException *excp ) {
		NSString *exn = [excp reason];
		alert("IMAP Exception was thrown\n");
		alert("Thrown: '%s'\n", [exn cString]);
		normal_end = NO;
	}
	@finally {
		dprintf("Disconnecting.\n");
		[self statusAt: 98.0 withText: @"Disconnecting."];
		[io disconnect];

	}

	if ( normal_end ) {
		total = newTotal;
		new = newNew;
		unread = newUnread;

		[self statusAt: 99.0 withText: @"Saving Information."];
		/* Save any updated folder info */
		[mPrefs setObject: [maskedFolders allObjects]
			forKey: @"Masked Folders"];
		[mPrefs setObject: [disabledFolders allObjects]
			forKey: @"Disabled Folders"];
		[prefs commitAccounts];
	}

	if ( actControl )
		[actControl completed];

	[mPool release];
	stop_now = 0;
	return retval;
}


- (void) stopNow: (id) obj
{
	stop_now = 1;
}


- (unsigned int) total
{
	return total;
}


- (unsigned int) unread
{
	return unread;
}


- (unsigned int) new
{
	return new;
}


- (IBAction)setCheckSubscribedOnly:(id)sender
{
	[mPrefs setObject: [NSNumber numberWithBool:
			[checkSubscribedButton state] == NSOnState]
		forKey: @"Subscribed Folders Only"];
	[prefs commitAccounts];
}


- (IBAction)setFetchHeaders:(id)sender
{
	[mPrefs setObject: [NSNumber numberWithBool:
			[fetchHeadersButton state] == NSOnState]
		forKey: @"Fetch Headers"];
	[prefs commitAccounts];
}

- (IBAction)setFetchMaskedHeaders:(id)sender
{
	[mPrefs setObject: [NSNumber numberWithBool:
			[fetchMaskedHeadersButton state] == NSOnState]
		forKey: @"Fetch Masked Headers"];
	[prefs commitAccounts];
}


- (IBAction)setLoginName:(id)sender
{
	[mPrefs setObject: [loginNameTextField stringValue]
		forKey: @"Username"];
	[prefs commitAccounts];
}


- (IBAction)setPassword:(id)sender
{
	if ( passwd ) [passwd release];
	passwd = [[NSString alloc] initWithString:
				[passwordTextField stringValue]];
	[keychain savePassword: passwd
		forUser: [mPrefs objectForKey: @"Username"]
		onHost: [mPrefs objectForKey: @"Hostname"]
		atPort: [[mPrefs objectForKey: @"Port"] intValue]
		withProto: ([[mPrefs objectForKey: @"Use SSL"] boolValue]) ?
			kSecProtocolTypeIMAPS : kSecProtocolTypeIMAP];
}


- (IBAction)setPathPrefix:(id)sender
{
	[mPrefs setObject: [prefixTextField stringValue]
		forKey: @"Path Prefix"];
	[prefs commitAccounts];
}


- (IBAction)setPort:(id)sender
{
	[mPrefs setObject: [NSNumber numberWithInt: [portTextField intValue]]
		forKey: @"Port"];
	[prefs commitAccounts];
}


- (IBAction)setServerName:(id)sender
{
	[mPrefs setObject: [serverNameTextField stringValue]
		forKey: @"Hostname"];
	[prefs commitAccounts];
}


- (IBAction)setUseSSL:(id)sender
{
	BOOL use_SSL = ([useSSLButton state] == NSOnState);
	[mPrefs setObject: [NSNumber numberWithBool: use_SSL]
		forKey: @"Use SSL"];
	[portTextField setIntValue: use_SSL ? 993 : 143];
	[mPrefs setObject: [NSNumber numberWithInt: [portTextField intValue]]
		forKey: @"Port"];
	[prefs commitAccounts];
}

@end
